其他
TensorFlow在美团外卖推荐场景的GPU训练优化实践
总第497篇
2022年 第014篇
1 背景
2 GPU训练优化挑战
3 系统设计与实现
3.1 参数规模的合理化
3.2 系统架构
3.3 关键实现
4 系统性能优化
4.1 数据层
4.2 计算层
4.3 通信层
4.4 性能指标
5 业务落地
5.1 完备性
5.2 训练效果
6 总结与展望
1 背景
2 GPU训练优化挑战
读取样本量大:训练样本在几十TB~几百TB,而CV等场景通常在几百GB以内。 模型参数量大:同时有大规模稀疏参数和稠密参数,需要几百GB甚至上TB存储,而CV等场景模型主要是稠密参数,通常在几十GB以内。 模型计算复杂度相对低一些:推荐系统模型在GPU上单步执行只需要10~100ms,而CV模型在GPU上单步执行是100~500ms,NLP模型在GPU上单步执行是500ms~1s。
GPU卡算力很强,但显存仍有限:如果要充分发挥GPU算力,需要把GPU计算用到的各种数据提前放置到显存中。而从2016年~2020年,NVIDIA Tesla GPU卡[5]计算能力提升了10倍以上,但显存大小只提升了3倍左右。 其它维度资源并不是很充足:相比GPU算力的提升速度,单机的CPU、网络带宽的增长速度较慢,如果遇到这两类资源负载较重的模型,将无法充分发挥GPU的能力,GPU服务器相比CPU服务器的性价比不会太高。
数据流系统:如何利用好多网卡、多路CPU,实现高性能的数据流水线,让数据的供给可以跟上GPU的消费速度。 混合参数计算:对于大规模稀疏参数,GPU显存直接装不下的情况,如何充分利用GPU高算力、GPU卡间的高带宽,实现一套大规模稀疏参数的计算,同时还需要兼顾稠密参数的计算。
3 系统设计与实现
3.1 参数规模的合理化
去交叉特征:交叉特征由单特征间做笛卡尔积产生,这会生成巨大的特征ID取值空间和对应Embedding参数表。深度预估模型发展至今,已经有大量的方法通过模型结构来建模单特征间的交互,避免了交叉特征造成的Embedding规模膨胀,如FM系列[16]、AutoInt[17]、CAN[18]等。 精简特征:特别是基于NAS的思路,以较低的训练成本实现深度神经网络自适应特征选择,如Dropout Rank[19]和FSCD[20]等工作。 压缩Embedding向量数:对特征取值进行复合ID编码和Embedding映射,以远小于特征取值空间的Embedding向量数,来实现丰富的特征Embedding表达,如Compositional Embedding[14]、Binary Code Hash Embedding[21]等工作。 压缩Embedding向量维度:一个特征Embedding向量的维度决定了其表征信息的上限,但是并非所有的特征取值都有那么大的信息量,需要Embedding表达。因此,可以每一个特征值自适应的学习精简Embedding维度,从而压缩参数总量,如AutoDim[22]和AMTL[23]等工作。 量化压缩:使用半精度甚至int8等更激进的方式,对模型参数做量化压缩,如DPQ[24]和MGQE[25]。
3.2 系统架构
数据模块:美团自研了一套支持多数据源、多框架的数据分发系统,在GPU系统上,我们改造数据模块支持了多网卡数据下载,以及考虑到NUMA Awareness的特性,在每颗CPU上都部署了一个数据分发服务。 计算模块:每张GPU卡启动一个TensorFlow训练进程执行训练。 通信模块:我们使用了Horovod[7]来做分布式训练的卡间通信,我们在每个节点上启动一个Horovod进程来执行对应的通信任务。
3.3 关键实现
3.3.1 参数存储
3.3.2 优化器
3.3.2 卡间通信
稀疏特征(ID类特征,规模较大,使用HashTable存储):由于每张卡的输入样本数据不同,因此输入的稀疏特征对应的特征向量,可能存放在其他GPU卡上。具体流程上,训练的前向我们通过卡间AllToAll通信,将每张卡的ID特征以Modulo的方式Partition到其他卡中,每张卡再去卡内的GPUHashTable查询稀疏特征向量,然后再通过卡间AllToAll通信,将第一次AllToAll从其他卡上拿到的ID特征以及对应的特征向量原路返回,通过两次卡间AllToAll通信,每张卡样本输入的ID特征都拿到对应的特征向量。训练的反向则会再次通过卡间AllToAll通信,将稀疏参数的梯度以Modulo的方式Partition到其他卡中,每张卡拿到自己的稀疏梯度后再执行稀疏优化器,完成大规模稀疏特征的优化。详细流程如下图所示:
稀疏特征(规模较小,使用Variable存储):相比使用HashTable的区别,由于每张GPU卡都有全量的参数,直接在卡内查找模型参数即可。在反向聚合梯度的时候,会通过卡间AllGather获取所有卡上的梯度求平均,然后交给优化器执行参数优化。 稠密特征:稠密参数也是每张卡都有全量的参数,卡内可以直接获取参数执行训练,最后通过卡间AllReduce聚合多卡的稠密梯度,执行稠密优化器。
训练模式:PS架构是异步训练模式,Booster架构是同步训练模式。 参数分布:PS架构下模型参数都存放在PS内存中,Booster架构下稀疏参数(HashTable)是Partition方式分布在单机八卡中,稠密参数(Variable)是Replica方式存放在每张卡中,因此Booster架构下的Worker角色兼顾了PS架构下PS/Worker角色的功能。 通信方式:PS架构下PS/Worker间通信走的是TCP(Grpc/Seastar),Booster架构下Worker间通信走的是NVSwitch(NCCL),任意两卡间双向带宽600GB/s,这也是Booster架构的训练速度取得较大提升的原因之一。
4 系统性能优化
4.1 数据层
4.1.1 样本拉取优化
4.1.2 特征解析优化
SIMD寻找最高位:通过SIMD指令将Varint类型数据的每个字节与0xF0做与运算,找到第一个结果等于0的字节,这个字节就是当前Varint数据的结束位置。 SIMD处理Varint:按理来说,通过SIMD指令将Varint数据高位清零后的每个字节依次右移3/2/1/0字节,就可得到最终的int类型数据,但SIMD没有这样的指令。因此,我们通过SIMD指令分别处理每个字节的高4bit、低4bit,完成了这个功能。我们将Varint数据的高低4bit分别处理成int_h4与int_l4,再做或运算,就得到了最终的int类型数据。具体优化流程如下图所示(4字节数据):
4.1.3 MemcpyH2D流水线
4.1.4 硬件调优
在网络传输方面,为了减少网络协议栈处理开销,提高数据拷贝的效率,我们通过优化网卡配置,开启LRO(Large-Receive-Offload)、TC Flower的硬件卸载、Tx-Nocache-Copy等特性,最终网络带宽提升了17%。 在CPU性能优化方面,经过性能profiling分析,发现内存延迟和带宽是瓶颈。于是我们尝试了3种NPS配置,综合业务场景和NUMA特性,选择了NPS2。此外,结合其他BIOS配置(例如APBDIS,P-state等),可以将内存延迟降低8%,内存带宽提升6%。
4.2 计算层
4.2.1 Embedding流水线
4.2.2 算子优化及XLA
局部优化:对于我们手动引入的动态shape算子(如Unique),我们进行了子图标记,不执行XLA编译,XLA只优化可以稳定加速的子图。 OOM兜底:XLA会根据算子的type、input type、shape等信息,缓存编译中间结果,避免重复编译。然而由于稀疏场景以及GPU架构实现的特殊性,天然存在Unique、DynamicPartition等Output shape是动态的算子,这就导致这些算子以及连接在这些算子之后的算子,在执行XLA编译时无法命中XLA缓存而重新编译,新的缓存越来越多,而旧的缓存不会被释放,最终导致CPU内存OOM。我们在XLA内部实现了LRUCache,主动淘汰掉旧的XLA缓存,避免OOM的问题。 Const Memcpy消除:XLA在使用TF_HLO重写TensorFlow算子时,对一些编译期已固定的数据会打上Const标记,然而这些Const算子的Output只能定义在Host端,为了将Host端的Output送给Device端需要再加一次MemcpyH2D,这就占用了TensorFlow原有的H2D Stream,影响样本数据提前拷贝到GPU端。由于XLA的Const Output在编译期已经固化,因此没有必要每一步都做一次MemcpyH2D,我们将Device端的Output缓存下来,后续使用该Output时,直接从缓存中读取,避免多余的MemcpyH2D。
4.3 通信层
4.3.1 HashTable相关算子融合
4.3.2 Variable相关算子融合
4.4 性能指标
5 业务落地
5.1 完备性
5.2 训练效果
6 总结与展望
7 作者简介
8 参考文献
阅读更多